/* Copyright (c) 2008 Charles Rich and Worcester Polytechnic Institute.
 * All Rights Reserved.  Use is subject to license terms.  See the file
 * "license.terms" for information on usage and redistribution of this
 * file and for a DISCLAIMER OF ALL WARRANTIES.
 */
package edu.wpi.cetask;

import org.w3c.dom.*;

import java.io.IOException;
import java.util.*;

import javax.imageio.metadata.IIOMetadataNode;
import javax.script.*;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPathConstants;

public class TaskEngine {
   
   public static boolean VERBOSE, DEBUG;
   
   /**
    * Create a new task recognizer with given default values for platform and
    * deviceType. Note that how platforms and deviceTypes are associated with
    * tasks is outside the scope of the standard, and is not addressed by
    * this implementation.
    */
   public TaskEngine (String platform, String deviceType) {
      this.platform = platform;
      this.deviceType = deviceType;
   }
   
   public TaskEngine () {}
   
   private String platform, deviceType;
   
   public String getDeviceType (Task task) { return deviceType; }

   public String getPlatform (Task task) { return platform; }
   
   private final ScriptEngine scriptEngine = 
      new ScriptEngineManager().getEngineByName("ECMAScript");
   
   public ScriptEngine getScriptEngine() { return scriptEngine; } 
   
   /**
    * Used to prevent recursive calls to
    * {@link DecompositionClass#updateBindings(Decomposition)} in
    * {@link Decomposition.Step#eval(String,String)}. Note this only works if
    * there is one thread running per TaskEngine.
    */
   static boolean updatingBindings;

   public Object eval (String script, Bindings bindings, String where) 
                            throws ScriptException {
      return eval(scriptEngine, script, bindings, where);
   }
   
   public Object evalGlobal (String script, String where) 
                                  throws ScriptException {
      ScriptEngine engine = getScriptEngine(); 
      return eval(engine, script, engine.getBindings(ScriptContext.GLOBAL_SCOPE),
            where);
   }

   private Object eval (ScriptEngine engine, String script, Bindings bindings,
         String where) throws ScriptException {
      try {
         bindings.put(ScriptEngine.FILENAME, where);
         return engine.eval(script, bindings);
      } finally { bindings.remove(ScriptEngine.FILENAME); }
   }

   // namespace of last loaded task model -- see Member.toString()
   private static String namespace = null;
   
   public static String getNamespace () { return namespace; } 
  
   private final Map<String,TaskModel> models = new HashMap<String,TaskModel>(); 
   
   // default properties for all task models
   final Properties properties = new Properties();
   { try {
      properties.load(TaskEngine.class.getResource("default.properties").openStream());
     } catch (IOException e) { throw new RuntimeException(e); }
   }
   
   public String getProperty (String key) { 
      return properties.getProperty(key);
   }
   
   /**
    * Load and return task model from XML document at given URI.  Loads the
    * first toplevel <taskModel> element.
    */
   public TaskModel parse (String uri) {
      try { 
         NodeList nodes = DocumentBuilderFactory.newInstance().newDocumentBuilder()
                            .parse(uri).getChildNodes();
         TaskModel model = null;;
         // use first task model 
         for (int i = 0; i < nodes.getLength(); i++) {
            Node node = nodes.item(i);
            if ( "taskModel".equals(node.getNodeName()) ) {
               model = new TaskModel(node, this);
               break;
            }
         }
         if ( model == null ) 
            throw new RuntimeException(uri+" does not contain a task model");
         namespace = model.getNamespace();
         models.put(getNamespace(), model);
         // TODO internationalize with resource bundles
         String props = Utils.replaceEndsWith(uri, ".xml", ".properties");
         try {
            properties.load(Utils.toURL(props).openStream());
            if ( VERBOSE ) System.out.println("    # Loaded "+props);
         } catch (IOException e) {} // ignore if no properties file
         // evaluate initialization script(s)
         for (Script script : model.getScripts())
            if ( script.isInit() ) evalGlobal(script.getText(), model+" init");
         if ( callback != null ) callback.onParse(uri, model);
         return model;
      } catch (Exception e) { throw new RuntimeException(e); }
   }
   
   /**
    * Interface for {@link #setOnParsing(OnParsing)} callback.
    */
   public interface OnParsing {
      void onParse (String uri, TaskModel model);
   }
   
   private OnParsing callback = null;
   
   /**
    * Set object to be called after new task model loaded.
    */
   public void setOnParsing (OnParsing callback) { this.callback = callback; }
   
   /**
    * Return task model for given namespace. If such a model has
    * not already been loaded, resolve namespace  and load.
    */
   public TaskModel getModel (String namespace) {
      TaskModel model = models.get(namespace);
      if ( model != null ) return model;
      return parse(namespace); // resolve
   }
 
   private final Map<Node,Description> cache = new HashMap<Node,Description>();
   
   Description get (Node node) { return cache.get(node); }
   
   void put (Node node, Description description ) {
      cache.put(node, description);
   }
   
   List<Node> xpathAll (String expression) {
      List<Node> nodes = new ArrayList<Node>();
      try { 
         for (TaskModel model : models.values()) {
            NodeList found =  (NodeList) Description.XPATH
              .evaluate(expression, model.node, XPathConstants.NODESET);
            for (int i = 0; i < found.getLength(); i++) // preserve order
               nodes.add(found.item(i));
         }
         return nodes;
      } catch (Exception e) { throw new RuntimeException(e); }
   }
  
   private long start; 
   
   /**
    * @see #setStart(long)
    */
   public long getStart () { return start; }
   
   /**
    * Set the start time to be used for 'when' slot in verbose version of
    * {@link Task#toString()}.  If start time is greater than zero, then shows 
    * 'when' in seconds since start time, rather than UTC in milliseconds.
    * 
    * @param start - start time in milliseconds (e.g., {@link System#currentTimeMillis()})
    * 
    * @see #VERBOSE
    */
   public void setStart (long start) { this.start = start; }
   
   // basic task tree management
   
   private Plan root, focus;
   { clear(); }
   
   public void clear () {
      // TODO find better dummy Node type?
      Node node = new IIOMetadataNode("Root");
      root = new Plan(new TaskClass(node, new TaskModel(node, this)).newInstance()); 
      focus = null;
   }
    
   /**
    * Return the current focus in the task tree, or null if no focus.<br>
    * <br>
    * Note that this is <em>not</em> the discourse concept of focus (cf.
    * Collagen), but rather a much simpler "execution" focus. For example, there
    * is no focus stack. Also, the focus will not usually not be on a done plan.
    * Management of the focus is the responsibility of the application using the
    * recognizer, with the exception of {@link #addTop(Plan)} and
    * {@link #removeTop(Plan)}, as documented.<br>
    * <br>
    * Note also that this is not the same as the "next" live action, since in a
    * partially ordered plan, there may be several next actions to choose from.
    * The current focus will usually have the next live actions as its children.
    * 
    * @see #setFocus(Plan)
    */
   public Plan getFocus () { return focus; }
   
   /**
    * Shift focus to the given plan or null.
    * 
    * @return true iff this is an "unexpected focus shift" in the sense of Lesh,
    *         Rich & Sidner's UM'01 paper, i.e., we are leaving the current
    *         focus when it is started but not done.
    *         
    * @see #getFocus()        
    */
   public boolean setFocus (Plan focus) {
      if ( focus == this.focus ) return false;
      if ( focus != null && (focus == root || !focus.isAncestor(root)) )
         throw new IllegalArgumentException(focus+" is not in task tree!");
      Plan old = this.focus;
      this.focus = focus;
      return old != null 
         && old.isStarted() && !old.isDone()
         && (focus == null || !( old.isAncestor(focus) || focus.isAncestor(old)) ); 
   }
    
   public Iterator<Plan> tops () { return root.children(); }
   
   /**
    * Add given plan to current task tree at toplevel.   Shifts the focus
    * to this plan if it is not done.
    */
   public void addTop (Plan plan) { 
      root.add(plan);
      if ( focus == null && !plan.isDone() ) focus = plan;
   }
   
   /**
    * Remove given toplevel plan from current tree. Also sets focus to null, if
    * given plan was focus.
    */
   public void removeTop (Plan plan) { 
      root.remove(plan);
      if ( focus == plan ) focus = null;
   }
      
   public Plan getTop (Plan plan) {
      // TODO more efficiently
      for (Iterator<Plan> i = tops(); i.hasNext();) {
         Plan top = i.next();
         if ( plan == top || plan.isAncestor(top) ) return top;
      }
      return null;
   }
 
   /**
    * Return first ancestor of given plan which is live or has live descendants,
    * stopping at toplevel plan containing given plan, or null if none.
    */
   public Plan newFocus (Plan plan) {
      Plan parent = plan.getParent();
      // TODO optimize this code?
      return parent == null || parent == root ? null :
            (parent.isLive() || parent.hasLiveDescendants()) ? parent : 
               newFocus(parent);
   }
   
   /**
    * Return list of plans in task tree with goal matching given task, searching
    * in the following categories, in order:
    * <ol>
    * <li> within current focus
    * <li> within current toplevel plan
    * <li> any toplevel plan
    * </ol>
    * If matching plans are found in one category, search does <em>not</em> continue to next category.
    * 
    * @param onlyLive only search live plans
    */
   public List<Plan> findMatches (Task task, boolean onlyLive) {
      if ( focus != null ) {
         List<Plan> matches = focus.findMatches(task, onlyLive);
         if ( !matches.isEmpty() ) return matches;
         matches = getTop(focus).findMatches(task, onlyLive);
         if ( !matches.isEmpty() ) return matches;
      }
      return root.findMatches(task, onlyLive);
   }

   /**
    * Call {@link Plan#decomposeAll()} on all plans in current tree.
    */
   public void decomposeAll() {
      for (Iterator<Plan> tops = tops(); tops.hasNext();) {
         Plan top = tops.next();
         if ( !top.isDone() ) top.decomposeAll();
      }
   }
 
}
